import { getSymbolsForSnippet } from "../../../autocomplete/context/ranking"
import { RecentlyEditedRange } from "../../../autocomplete/util/types"
import * as vscode from "vscode"

import { IDE } from "../../../"

type VsCodeRecentlyEditedRange = {
	uri: vscode.Uri
	range: vscode.Range
} & Omit<RecentlyEditedRange, "filepath" | "range">

interface VsCodeRecentlyEditedDocument {
	timestamp: number
	uri: vscode.Uri
}

export class RecentlyEditedTracker {
	private static staleTime = 1000 * 60 * 2
	private static maxRecentlyEditedRanges = 3
	private recentlyEditedRanges: VsCodeRecentlyEditedRange[] = []

	private recentlyEditedDocuments: VsCodeRecentlyEditedDocument[] = []
	private static maxRecentlyEditedDocuments = 10
	private disposable: vscode.Disposable | undefined
	private cleanupInterval: NodeJS.Timeout | undefined

	constructor(private ide: IDE) {
		this.disposable = vscode.workspace.onDidChangeTextDocument((event) => {
			event.contentChanges.forEach((change) => {
				const editedRange = {
					uri: event.document.uri,
					range: new vscode.Range(
						new vscode.Position(change.range.start.line, 0),
						new vscode.Position(change.range.end.line + 1, 0),
					),
					timestamp: Date.now(),
				}
				this.insertRange(editedRange)
			})

			this.insertDocument(event.document.uri)
		})

		this.cleanupInterval = setInterval(() => {
			this.removeOldEntries()
		}, 1000 * 15)
	}

	private async insertRange(editedRange: Omit<VsCodeRecentlyEditedRange, "lines" | "symbols">): Promise<void> {
		if (editedRange.uri.scheme !== "file") {
			return
		}

		// Check for overlap with any existing ranges
		for (let i = 0; i < this.recentlyEditedRanges.length; i++) {
			let range = this.recentlyEditedRanges[i]
			if (range.range.intersection(editedRange.range)) {
				const union = range.range.union(editedRange.range)
				const contents = await this._getContentsForRange({
					...range,
					range: union,
				})
				range = {
					...range,
					range: union,
					lines: contents.split("\n"),
					symbols: getSymbolsForSnippet(contents),
				}
				this.recentlyEditedRanges[i] = range
				return
			}
		}

		// Otherwise, just add the new and maintain max size
		const contents = await this._getContentsForRange(editedRange)
		const newLength = this.recentlyEditedRanges.unshift({
			...editedRange,
			lines: contents.split("\n"),
			symbols: getSymbolsForSnippet(contents),
		})
		if (newLength >= RecentlyEditedTracker.maxRecentlyEditedRanges) {
			this.recentlyEditedRanges = this.recentlyEditedRanges.slice(
				0,
				RecentlyEditedTracker.maxRecentlyEditedRanges,
			)
		}
	}

	private insertDocument(uri: vscode.Uri): void {
		// Don't add a duplicate
		if (this.recentlyEditedDocuments.some((doc) => doc.uri === uri)) {
			return
		}

		const newLength = this.recentlyEditedDocuments.unshift({
			uri,
			timestamp: Date.now(),
		})
		if (newLength >= RecentlyEditedTracker.maxRecentlyEditedDocuments) {
			this.recentlyEditedDocuments = this.recentlyEditedDocuments.slice(
				0,
				RecentlyEditedTracker.maxRecentlyEditedDocuments,
			)
		}
	}

	private removeOldEntries() {
		this.recentlyEditedRanges = this.recentlyEditedRanges.filter(
			(entry) => entry.timestamp > Date.now() - RecentlyEditedTracker.staleTime,
		)
	}

	private async _getContentsForRange(entry: Omit<VsCodeRecentlyEditedRange, "lines" | "symbols">): Promise<string> {
		const content = await this.ide.readFile(entry.uri.toString())
		if (content === null) {
			return ""
		}
		return content
			.toString()
			.split("\n")
			.slice(entry.range.start.line, entry.range.end.line + 1)
			.join("\n")
	}

	public async getRecentlyEditedRanges(): Promise<RecentlyEditedRange[]> {
		return this.recentlyEditedRanges.map((entry) => {
			return {
				...entry,
				filepath: entry.uri.toString(),
			}
		})
	}

	public dispose(): void {
		this.disposable?.dispose()
		if (this.cleanupInterval) {
			clearInterval(this.cleanupInterval)
		}
		this.recentlyEditedRanges = []
		this.recentlyEditedDocuments = []
	}
}
